using System;
using System.Collections.Generic;
using System.Text;
using System.Xml;
using Microsoft.SqlServer.Dts.Pipeline.Wrapper;
using Microsoft.SqlServer.Dts.Runtime;

namespace Microsoft.SharedSource.SqlServer.MDDE.Components
{
    [
        System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1705:LongAcronymsShouldBePascalCased"),
        SupportedComponentType("{90C7770B-DE7C-435E-880E-E718C92C0573}"),
        SupportedComponentType("{2C0A8BE5-1EDC-4353-A0EF-B778599C65A0}")
    ]
    public class MDDEDataFlowSource : MDDEDataFlowComponent
    {
        #region Member Variables

        private XmlNodeList changedProperties = null;
        private IDTSCustomPropertyCollection100 customProperties = null;
        private XmlNodeList customMappings = null;
        private bool preservePreviousMappings = true;

        //private Package originalPackage = new Package();
        IDTSComponentMetaData100 component = null;

        #endregion

        public MDDEDataFlowSource(Package package, Object component, string name, string uri)
            : base(package, component, name, uri)
        {
        }

        #region Overriden Methods

        public override void GetChangeXml(MDDEComponent subject, XmlDocument changeXml)
        {
            #region Set Values

            if (subject == null)
                throw new ArgumentNullException("Subject can not NULL");

            if (!(subject is MDDEDataFlowSource))
                throw new ArgumentException("Subject is not of type MDDEDataFlowSource");

            component = (IDTSComponentMetaData100)innerComponent;
            IDTSComponentMetaData100 referenceComponent = (IDTSComponentMetaData100)subject.InnerComponent;

            IDTSCustomPropertyCollection100 componentPropertyCollection = (IDTSCustomPropertyCollection100)component.CustomPropertyCollection;
            IDTSCustomPropertyCollection100 referenceComponentPropertyCollection = (IDTSCustomPropertyCollection100)referenceComponent.CustomPropertyCollection;

            XmlNode componentNode = null;
            componentNode = changeXml.CreateElement("Component");

            #endregion

            #region Compare properties of Component with subject and create Properties' Xml

            for (int propertyIndex = 0; propertyIndex < componentPropertyCollection.Count; propertyIndex++)
            {
                if (componentPropertyCollection[propertyIndex].Name.CompareTo("Name") == 0)
                    continue;

                IDTSCustomProperty100 componentProperty = componentPropertyCollection[propertyIndex];
                IDTSCustomProperty100 referenceComponentProperty = referenceComponentPropertyCollection[propertyIndex];

                string componentPropertyValue = String.Empty;
                if (componentProperty.Value != null)
                    componentPropertyValue = componentProperty.Value.ToString();

                string referenceComponentPropertyValue = String.Empty;
                if (referenceComponentProperty.Value != null)
                    referenceComponentPropertyValue = referenceComponentProperty.Value.ToString();

                if (componentPropertyValue != referenceComponentPropertyValue)
                {
                    UpdateNodeProperties(changeXml, componentNode, componentProperty.Name, componentPropertyValue);
                }
            }

            #endregion

            #region Extract Custom Mappings from Component

            IDTSOutput100 output = component.OutputCollection[0];
            bool externalColumnUsed;
            int externalColumnID;
            string outputColumnName;

            UpdateNodeMappings(changeXml, componentNode, false);

            foreach (IDTSExternalMetadataColumn100 externalColumn in output.ExternalMetadataColumnCollection)
            {
                externalColumnUsed = false;
                externalColumnID = externalColumn.ID;
                outputColumnName = String.Empty;

                foreach (IDTSOutputColumn100 outputColumn in output.OutputColumnCollection)
                {
                    if (outputColumn.ExternalMetadataColumnID == externalColumnID)
                    {
                        outputColumnName = outputColumn.Name;
                        externalColumnUsed = true;
                        break;
                    }
                }

                if (externalColumnUsed)
                {
                    if (externalColumn.Name != outputColumnName)
                    {
                        UpdateNodeMapping(changeXml, componentNode, false,
                            "True", externalColumn.Name, outputColumnName);
                    }
                }
                else
                {
                    UpdateNodeMapping(changeXml, componentNode, false,
                        "False", externalColumn.Name, String.Empty);
                }
            }

            #endregion

            #region Update Changes Xml
            
            AddAttribute(changeXml, componentNode, "Name", this.name);
            AddAttribute(changeXml, componentNode, "URL", this.uri);
            AddAttribute(changeXml, componentNode, "RowsetDescriptor", "");

            changeXml.FirstChild.AppendChild(componentNode);            

            #endregion
        }

        public override bool ApplyChanges(XmlDocument changeXml, MDDEExecutionDAG originalPackage)
        {
            try
            {
                #region Set Values

                XmlNode componentChangeXml =
                    changeXml.SelectSingleNode("//PackageChanges/Component[@URL='" + this.uri + "']");

                //string packageXml = null;
                //package.SaveToXML(out packageXml, null);
                //originalPackage.LoadFromXML(packageXml, null);

                //if (innerComponent == null)
                //    return false;

                //if (!(innerComponent is IDTSComponentMetaData100))
                //    return false;

                component = (IDTSComponentMetaData100)innerComponent;

                #endregion

                #region Read Change Xml

                if (componentChangeXml != null)
                {
                    if (!ReadChangeXml(componentChangeXml))
                        return false;
                }

                #endregion

                #region Apply Properties and Verify If Sufficient Property Values' Are Available

                if (!ApplyProperties())
                    return false;

                #endregion

                #region Add Connection Manager to Runtime Collection

                string connectionManagerID = component.RuntimeConnectionCollection[0].ConnectionManagerID;

                if (String.IsNullOrEmpty(connectionManagerID))
                    return false;

                component.RuntimeConnectionCollection[0].ConnectionManager = DtsConvert.ToConnectionManager90(this.package.Connections[connectionManagerID]);

                #endregion

                #region Reinitialize Component

                IDTSDesigntimeComponent100 designTimeComponent = component.Instantiate();

                designTimeComponent.AcquireConnections(null);

                DTSValidationStatus validationStatus = designTimeComponent.Validate();
                if (validationStatus == DTSValidationStatus.VS_ISBROKEN)
                {
                    designTimeComponent.ReleaseConnections();
                    return false;
                }

                designTimeComponent.ReinitializeMetaData();
                designTimeComponent.ReleaseConnections();

                #endregion

                #region Clear Output Collections (except Error Collections)

                ClearOutputCollections(designTimeComponent);

                #endregion

                #region Map External Columns With Output Columns

                #region Create Output Collections based on External Metadata Collections and Map External and Output Columns

                CreateOutputCollections(designTimeComponent);

                #endregion

                #region Restore Previous Mapping Settings based on User Preference

                if (preservePreviousMappings)
                {
                    IDTSComponentMetaData100 originalComponent =
                        (originalPackage.GetMDDEComponentByUri(uri)).InnerComponent as IDTSComponentMetaData100;

                    RestorePreviousMappings(designTimeComponent, originalComponent);
                }

                #endregion

                #region Override Mappings with New Custom Mappings

                if (customMappings != null)
                {
                    ApplyCustomMappings(designTimeComponent);
                }

                #endregion

                #region Synchronize Error Output Columns that are Not Used by removing them

                SynchronizeErrorOutput();

                #endregion

                #endregion

                return true;
            }

            catch
            {
                // TODO: Add to Log
                return false;
            }
        }

        public override bool Validate()
        {
            try
            {
                #region Add Connection Manager to Runtime Collection

                string connectionManagerID
                    = ((IDTSComponentMetaData100)innerComponent).RuntimeConnectionCollection[0].ConnectionManagerID;

                if (String.IsNullOrEmpty(connectionManagerID))
                    return false;

                ((IDTSComponentMetaData100)innerComponent).RuntimeConnectionCollection[0].ConnectionManager
                    = DtsConvert.ToConnectionManager90(this.package.Connections[connectionManagerID]);

                #endregion

                IDTSDesigntimeComponent100 designTimeComponent = ((IDTSComponentMetaData100)innerComponent).Instantiate();

                designTimeComponent.AcquireConnections(null);
                DTSValidationStatus validationStatus = designTimeComponent.Validate();
                designTimeComponent.ReleaseConnections();

                if (validationStatus == DTSValidationStatus.VS_ISVALID)
                    return true;
                else
                    return false;
            }

            catch
            {
                // TODO: Add to Log
                return false;
            }
        }

        public override XmlDocument GetComponentTree()
        {
            XmlDocument xComponent = base.GetComponentTree();

            XmlElement xRoot = xComponent.DocumentElement;

            #region Append the mappings.

            if (InnerDataFlowComponent.OutputCollection.Count > 0)
            {
                // Proceed only if there exists an output that is being used.
                if (!(InnerDataFlowComponent.OutputCollection[0].IsErrorOut) &&
                      InnerDataFlowComponent.OutputCollection[0].
                      ExternalMetadataColumnCollection.IsUsed)
                {
                    IDTSExternalMetadataColumnCollection100 extMDColl =
                        InnerDataFlowComponent.OutputCollection[0].
                        ExternalMetadataColumnCollection;

                    // Create the 'Mappings' node.
                    XmlElement xMappings = AppendChildElement(xRoot, "Mappings", null);

                    foreach (IDTSExternalMetadataColumn100 extMDCol in extMDColl)
                    {
                        bool usage = false;
                        foreach (IDTSOutputColumn100 outputCol in
                            InnerDataFlowComponent.OutputCollection[0].OutputColumnCollection)
                        {
                            if (outputCol.ExternalMetadataColumnID == extMDCol.ID)
                            {
                                AppendChildElement(xMappings, "Mapping",
                                    new string[]{"Usage","True","SourceColumn",extMDCol.Name,
                                        "DestinationColumn",outputCol.Name});
                                usage = true;
                                break;
                            }
                        }
                        if (!usage)
                        {
                            AppendChildElement(xMappings, "Mapping",
                                new string[]{ "Usage", "False", "SourceColumn", extMDCol.Name,
                                    "DestinationColumn", string.Empty});
                        }
                    }// end of foreach

                    if (!xMappings.HasChildNodes)
                        xMappings.ParentNode.RemoveChild(xMappings);
                }
            }

            #endregion

            return xComponent;
        }

        #endregion

        #region Helper Methods

        private bool ReadChangeXml(XmlNode changeXml)
        {
            if (changeXml.SelectSingleNode("Properties") != null)
                changedProperties = changeXml["Properties"].ChildNodes;

            if (component.CustomPropertyCollection == null)
                return false;

            customProperties = (IDTSCustomPropertyCollection100)component.CustomPropertyCollection;

            if (customProperties == null)
                return false;

            if (changeXml.SelectSingleNode("Mappings") != null)
            {
                if (changeXml["Mappings"].ChildNodes.Count > 0)
                    customMappings = changeXml["Mappings"].ChildNodes;

                if (changeXml["Mappings"].Attributes["PreservePreviousMappings"] != null)
                    preservePreviousMappings = Convert.ToBoolean(
                        changeXml["Mappings"].Attributes["PreservePreviousMappings"].Value);
            }

            return true;
        }

        private bool ApplyProperties()
        {
            if (changedProperties != null)
            {
                foreach (XmlNode changedProperty in changedProperties)
                    customProperties[changedProperty.Attributes["Name"].Value].Value = this.ConvertToCompatibleType(changedProperty.Attributes["Value"].Value,
                        customProperties[changedProperty.Attributes["Name"].Value].Value.GetType().Name);
            }

            if (component.Instantiate().Validate()
                == DTSValidationStatus.VS_ISBROKEN)
                return false;
            else
                return true;
        }

        private void RestorePreviousMappings(IDTSDesigntimeComponent100 designTimeComponent,
            IDTSComponentMetaData100 originalComponent)
        {
            //ObjectType objectType = ObjectType.Package;
            //IDTSComponentMetaData100 originalComponent
            //    = (IDTSComponentMetaData100)this.GetComponent(this.originalPackage, uri, ref objectType);

            foreach (IDTSExternalMetadataColumn100 originalExternalColumn in originalComponent.OutputCollection[0].ExternalMetadataColumnCollection)
            {
                #region Find if Original External Column is Mapped to any Original Output Column

                IDTSOutputColumn100 originalOutputColumn = null;
                bool originalExternalColumnUsed = false;

                for (int originalOutputColumnIndex = 0;
                        originalOutputColumnIndex < originalComponent.OutputCollection[0].OutputColumnCollection.Count;
                        originalOutputColumnIndex++)
                {
                    if (originalComponent.OutputCollection[0].OutputColumnCollection[originalOutputColumnIndex].ExternalMetadataColumnID
                        == originalExternalColumn.ID)
                    {
                        originalOutputColumn = originalComponent.OutputCollection[0].OutputColumnCollection[originalOutputColumnIndex];
                        originalExternalColumnUsed = true;
                        break;
                    }
                }

                #endregion

                #region Find Columns' Presence in New Package

                bool externalColumnPresent = false;
                bool outputColumnPresent = false;
                int columnIndex;
                int externalColumnID = 0;

                for (columnIndex = 0; columnIndex < component.OutputCollection[0].ExternalMetadataColumnCollection.Count;
                    columnIndex++)
                {
                    if (originalExternalColumn.Name
                        == component.OutputCollection[0].ExternalMetadataColumnCollection[columnIndex].Name)
                    {
                        externalColumnPresent = true;
                        externalColumnID = component.OutputCollection[0].ExternalMetadataColumnCollection[columnIndex].ID;
                        break;
                    }
                }

                string originalOutputColumnName;
                if (originalOutputColumn != null)
                    originalOutputColumnName = originalOutputColumn.Name;
                else
                    originalOutputColumnName = originalExternalColumn.Name;

                for (columnIndex = 0; columnIndex < component.OutputCollection[0].OutputColumnCollection.Count;
                    columnIndex++)
                {
                    if (originalOutputColumnName
                            == component.OutputCollection[0].OutputColumnCollection[columnIndex].Name)
                    {
                        outputColumnPresent = true;
                        break;
                    }
                }

                #endregion

                #region External Column was Used in Original Package

                if (originalExternalColumnUsed)
                {
                    if (externalColumnPresent)
                    {
                        #region Remove Output Column from New Package

                        if (outputColumnPresent)
                        {
                            designTimeComponent.DeleteOutputColumn(component.OutputCollection[0].ID,
                                component.OutputCollection[0].OutputColumnCollection[originalOutputColumnName].ID);
                        }

                        #endregion

                        #region Remove External Column's Link with Output Columns from New Package

                        for (columnIndex = component.OutputCollection[0].OutputColumnCollection.Count - 1; columnIndex >= 0;
                            columnIndex--)
                        {
                            if (component.OutputCollection[0].OutputColumnCollection[columnIndex].ExternalMetadataColumnID
                                == externalColumnID)
                            {
                                designTimeComponent.DeleteOutputColumn(component.OutputCollection[0].ID,
                                    component.OutputCollection[0].OutputColumnCollection[columnIndex].ID);
                            }
                        }

                        #endregion

                        #region Create Output Column corresponding to Original Package And Map

                        IDTSOutputColumn100 newOutputColumn = designTimeComponent.InsertOutputColumnAt(component.OutputCollection[0].ID,
                            0, originalOutputColumn.Name, originalOutputColumn.Description);

                        newOutputColumn.SetDataTypeProperties(originalOutputColumn.DataType,
                            originalOutputColumn.Length, originalOutputColumn.Precision,
                            originalOutputColumn.Scale, originalOutputColumn.CodePage);

                        designTimeComponent.MapOutputColumn(component.OutputCollection[0].ID,
                            newOutputColumn.ID, externalColumnID, false);

                        #endregion
                    }
                }

                #endregion

                #region External Column was Not Used in Original Package

                else
                {
                    if (outputColumnPresent)
                    {
                        designTimeComponent.DeleteOutputColumn(component.OutputCollection[0].ID,
                            component.OutputCollection[0].OutputColumnCollection[originalExternalColumn.Name].ID);
                    }
                }

                #endregion
            }
        }

        private void ClearOutputCollections(IDTSDesigntimeComponent100 designTimeComponent)
        {
            for (int collectionIndex = 0; collectionIndex < component.OutputCollection.Count;
                    collectionIndex++)
            {
                if (!component.OutputCollection[collectionIndex].IsErrorOut)
                {
                    for (int columnIndex = component.OutputCollection[0].OutputColumnCollection.Count - 1; columnIndex >= 0;
                            columnIndex--)
                    {
                        designTimeComponent.DeleteOutputColumn(component.OutputCollection[collectionIndex].ID,
                            component.OutputCollection[collectionIndex].OutputColumnCollection[columnIndex].ID);
                    }
                }
            }
        }

        private void CreateOutputCollections(IDTSDesigntimeComponent100 designTimeComponent)
        {
            for (int collectionIndex = 0; collectionIndex < component.OutputCollection.Count;
                    collectionIndex++)
            {
                if (!component.OutputCollection[collectionIndex].IsErrorOut)
                {
                    for (int columnIndex = 0; columnIndex < component.OutputCollection[0].ExternalMetadataColumnCollection.Count;
                        columnIndex++)
                    {
                        IDTSOutputColumn100 outputColumn = designTimeComponent.InsertOutputColumnAt(component.OutputCollection[collectionIndex].ID,
                            columnIndex, component.OutputCollection[collectionIndex].ExternalMetadataColumnCollection[columnIndex].Name,
                            "");

                        outputColumn.SetDataTypeProperties(component.OutputCollection[collectionIndex].ExternalMetadataColumnCollection[columnIndex].DataType,
                            component.OutputCollection[collectionIndex].ExternalMetadataColumnCollection[columnIndex].Length,
                            component.OutputCollection[collectionIndex].ExternalMetadataColumnCollection[columnIndex].Precision,
                            component.OutputCollection[collectionIndex].ExternalMetadataColumnCollection[columnIndex].Scale,
                            component.OutputCollection[collectionIndex].ExternalMetadataColumnCollection[columnIndex].CodePage);

                        designTimeComponent.MapOutputColumn(component.OutputCollection[collectionIndex].ID,
                            component.OutputCollection[collectionIndex].OutputColumnCollection[columnIndex].ID, component.OutputCollection[collectionIndex].ExternalMetadataColumnCollection[columnIndex].ID,
                            true);
                    }
                }
            }
        }

        private void SynchronizeErrorOutput()
        {
            foreach (IDTSExternalMetadataColumn100 externalColumn in component.OutputCollection[0].ExternalMetadataColumnCollection)
            {
                bool mappingPresent = false;

                for (int columnIndex = 0; columnIndex < component.OutputCollection[0].OutputColumnCollection.Count;
                    columnIndex++)
                {
                    if (component.OutputCollection[0].OutputColumnCollection[columnIndex].ExternalMetadataColumnID
                        == externalColumn.ID)
                    {
                        mappingPresent = true;
                        break;
                    }
                }

                if (!mappingPresent)
                {
                    // Check if error collection contains the column which is to be removed
                    for (int columnIndex = 0; columnIndex < component.OutputCollection[1].OutputColumnCollection.Count;
                        columnIndex++)
                    {
                        if (externalColumn.Name
                                == component.OutputCollection[1].OutputColumnCollection[columnIndex].Name)
                        {
                            component.OutputCollection[1].OutputColumnCollection.RemoveObjectByID(
                                component.OutputCollection[1].OutputColumnCollection[externalColumn.Name].ID);

                            break;
                        }
                    }
                }
            }
        }

        private void ApplyCustomMappings(IDTSDesigntimeComponent100 designTimeComponent)
        {
            foreach (XmlNode componentMapping in customMappings)
            {
                bool usage = Convert.ToBoolean(componentMapping.Attributes["Usage"].Value.ToString());
                string externalColumnName = componentMapping.Attributes["SourceColumn"].Value.ToString();
                string outputColumnName = String.Empty;
                if (usage)
                    outputColumnName = componentMapping.Attributes["DestinationColumn"].Value.ToString(); ;

                #region Find Columns' Presence

                bool externalColumnPresent = false;
                bool outputColumnPresent = false;
                int columnIndex;

                IDTSExternalMetadataColumn100 externalColumn = null;
                int outputColumnID = 0;

                for (columnIndex = 0; columnIndex < component.OutputCollection[0].ExternalMetadataColumnCollection.Count;
                    columnIndex++)
                {
                    if (externalColumnName
                        == component.OutputCollection[0].ExternalMetadataColumnCollection[columnIndex].Name)
                    {
                        externalColumnPresent = true;
                        externalColumn = component.OutputCollection[0].ExternalMetadataColumnCollection[columnIndex];
                        break;
                    }
                }

                for (columnIndex = 0; columnIndex < component.OutputCollection[0].OutputColumnCollection.Count;
                    columnIndex++)
                {
                    if (outputColumnName
                            == component.OutputCollection[0].OutputColumnCollection[columnIndex].Name)
                    {
                        outputColumnPresent = true;
                        outputColumnID = component.OutputCollection[0].OutputColumnCollection[columnIndex].ID;
                        break;
                    }
                }

                #endregion

                #region If External Column is to be Used

                if (usage)
                {
                    if (externalColumnPresent)
                    {
                        #region Remove External Column's Link with Output Columns (Mark Unusable)

                        for (columnIndex = component.OutputCollection[0].OutputColumnCollection.Count - 1; columnIndex >= 0;
                            columnIndex--)
                        {
                            if (component.OutputCollection[0].OutputColumnCollection[columnIndex].ExternalMetadataColumnID
                                == externalColumn.ID)
                            {
                                component.OutputCollection[0].OutputColumnCollection[columnIndex].ExternalMetadataColumnID = 0;
                            }
                        }

                        #endregion

                        #region Create Output Column (if not present, similar to External Column) and Map

                        if (!outputColumnPresent)
                        {
                            IDTSOutputColumn100 newOutputColumn = designTimeComponent.InsertOutputColumnAt(component.OutputCollection[0].ID,
                                0, outputColumnName, externalColumn.Description);

                            newOutputColumn.SetDataTypeProperties(externalColumn.DataType,
                                externalColumn.Length, externalColumn.Precision,
                                externalColumn.Scale, externalColumn.CodePage);

                            outputColumnID = newOutputColumn.ID;
                        }

                        designTimeComponent.MapOutputColumn(component.OutputCollection[0].ID,
                            outputColumnID, externalColumn.ID, false);

                        #endregion
                    }
                }

                #endregion

                #region If External Column is Not To Be Used

                else
                {
                    if (externalColumnPresent)
                    {
                        #region Mark Ununsed Column as Unusable

                        for (columnIndex = component.OutputCollection[0].OutputColumnCollection.Count - 1; columnIndex >= 0;
                                columnIndex--)
                        {
                            if (component.OutputCollection[0].OutputColumnCollection[columnIndex].ExternalMetadataColumnID
                                == externalColumn.ID)
                            {
                                component.OutputCollection[0].OutputColumnCollection[columnIndex].ExternalMetadataColumnID = 0;
                            }
                        }

                        #endregion
                    }
                }

                #endregion
            }

            #region Remove Output Columns that are Marked Unusable

            for (int columnIndex = component.OutputCollection[0].OutputColumnCollection.Count - 1; columnIndex >= 0;
                            columnIndex--)
            {
                if (component.OutputCollection[0].OutputColumnCollection[columnIndex].ExternalMetadataColumnID
                    == 0)
                {
                    designTimeComponent.DeleteOutputColumn(component.OutputCollection[0].ID,
                        component.OutputCollection[0].OutputColumnCollection[columnIndex].ID);
                }
            }

            #endregion
        }

        #endregion
    }
}
